Skip to content

feat(workflow-executor): implement AgentPort adapter using agent-client#1496

Merged
matthv merged 12 commits intofeat/prd-214-setup-workflow-executor-packagefrom
feature/prd-232-implementer-agentport-avec-agent-client
Mar 18, 2026
Merged

feat(workflow-executor): implement AgentPort adapter using agent-client#1496
matthv merged 12 commits intofeat/prd-214-setup-workflow-executor-packagefrom
feature/prd-232-implementer-agentport-avec-agent-client

Conversation

@matthv
Copy link
Member

@matthv matthv commented Mar 18, 2026

Summary

  • Add AgentClientAgentPort class wrapping @forestadmin/agent-client's RemoteAgentClient to implement the AgentPort interface
  • Capabilities are cached per collection with automatic eviction on rejection
  • Add @forestadmin/agent-client as dependency of @forestadmin/workflow-executor

Known MVP limitations

  • displayName = fieldName (capabilities don't expose display names)
  • referencedCollectionName is undefined (not available in capabilities)
  • Related data uses relationName as collectionName (no schema endpoint yet)

Test plan

  • getRecord — returns RecordData, throws on missing record, caches capabilities, retries after rejection
  • updateRecord — returns RecordData, calls update() with correct args
  • getRelatedData — returns RecordData[], handles empty results
  • getActions — reads from actionEndpoints, returns [] for unknown collections
  • executeAction — calls action() then execute(), propagates errors
  • yarn workspace @forestadmin/workflow-executor test — 18 tests passing
  • yarn workspace @forestadmin/workflow-executor lint — clean
  • yarn workspace @forestadmin/workflow-executor build — clean

fixes PRD-232

🤖 Generated with Claude Code

Note

Implement AgentClientAgentPort adapter backed by agent-client in workflow-executor

  • Adds AgentClientAgentPort, a new AgentPort implementation that delegates to RemoteAgentClient for record fetch, update, relation listing, and action execution.
  • Reworks core types in record.ts: introduces ActionRef and CollectionRef, replaces RecordRef, and updates RecordData to carry composite recordId as Array<string|number>.
  • Updates AgentPort and WorkflowPort interfaces to support composite primary keys and richer action data across all method signatures.
  • Adds RecordNotFoundError in errors.ts thrown when getRecord finds no matching record.
  • Behavioral Change: AgentPort method signatures now require Array<string|number> for record IDs instead of scalar values; existing implementations must be updated.

Changes since #1496 opened

  • Replaced RecordRef type with CollectionRef type in the PendingStepExecution interface's availableRecords property and updated corresponding import statement [2703ae7]
  • Moved @forestadmin/agent-client dependency from production dependencies to a different dependency classification in @forestadmin/workflow-executor package [889ea3b]
  • Changed selectedRecord property type from RecordData to CollectionRef in AiTaskStepExecutionData interface [e7fcf1b]

Macroscope summarized 2a2d898.

@linear
Copy link

linear bot commented Mar 18, 2026

@Scra3 Scra3 changed the base branch from main to feat/prd-214-setup-workflow-executor-package March 18, 2026 10:01
@qltysh
Copy link

qltysh bot commented Mar 18, 2026

Qlty

Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (3)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/errors.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/index.ts100.0%
New file Coverage rating: A
...ages/workflow-executor/src/adapters/agent-client-agent-port.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@matthv
Copy link
Member Author

matthv commented Mar 18, 2026

Code review

Found 1 issue:

  1. getRecord hardcodes field: 'id' in its filter, assuming every collection uses a primary key named id. Forest Admin supports arbitrary primary key names and composite primary keys. The rest of the codebase uses ConditionTreeFactory.matchIds() to handle this dynamically. Collections with a non-id PK will always get a false "Record not found" error.

const records = await this.client.collection(collectionName).list<Record<string, unknown>>({
filters: { field: 'id', operator: 'Equal', value: recordId },
pagination: { size: 1, number: 1 },
});

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Comment on lines +30 to +35
function extractRecordId(
primaryKeyFields: string[],
record: Record<string, unknown>,
): Array<string | number> {
return primaryKeyFields.map(field => record[field] as string | number);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low adapters/agent-client-agent-port.ts:30

extractRecordId uses a type assertion as string | number on record[field], but when the field is missing, this passes undefined through silently. When the resulting array is passed to encodePk, String(undefined) produces the literal string "undefined", corrupting the record ID. This is reachable in getRelatedData when the API response omits expected primary key fields, or when getCollectionRef defaults to ['id'] for an unknown collection but the actual records use a different key.

function extractRecordId(
  primaryKeyFields: string[],
  record: Record<string, unknown>,
): Array<string | number> {
-  return primaryKeyFields.map(field => record[field] as string | number);
+  return primaryKeyFields.map(field => {
+    const value = record[field];
+    if (value === undefined || value === null) {
+      throw new Error(`Missing primary key field: ${field}`);
+    }
+    if (typeof value !== 'string' && typeof value !== 'number') {
+      throw new Error(`Invalid primary key type for ${field}: ${typeof value}`);
+    }
+    return value;
+  });
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/adapters/agent-client-agent-port.ts around lines 30-35:

`extractRecordId` uses a type assertion `as string | number` on `record[field]`, but when the field is missing, this passes `undefined` through silently. When the resulting array is passed to `encodePk`, `String(undefined)` produces the literal string `"undefined"`, corrupting the record ID. This is reachable in `getRelatedData` when the API response omits expected primary key fields, or when `getCollectionRef` defaults to `['id']` for an unknown collection but the actual records use a different key.

Evidence trail:
packages/workflow-executor/src/adapters/agent-client-agent-port.ts lines 25-33 (extractRecordId and encodePk functions), lines 73-84 (getRelatedData usage), lines 104-112 (getCollectionRef default to ['id']). JavaScript behavior: `String(undefined)` returns `"undefined"`.

matthv and others added 9 commits March 18, 2026 14:59
Add AgentClientAgentPort class that wraps @forestadmin/agent-client's
RemoteAgentClient to satisfy the AgentPort interface. Includes capabilities
caching with rejection eviction, and full test coverage.

fixes PRD-232

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split RecordRef into CollectionRef (collection-level info: name,
displayName, fields) and move recordId into RecordData. This fixes
the type inconsistency where getCollectionRef() returned a RecordRef
that had no meaningful recordId.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…elationship types

- Create RecordNotFoundError in src/errors.ts for typed error handling
- Check all 4 relationship types (ManyToOne, OneToOne, OneToMany, ManyToMany)
  using a RELATIONSHIP_TYPES Set instead of hardcoded ManyToOne check
- Add parameterized tests for each relationship type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… capabilities

Replace runtime capabilities fetching with CollectionRef injected at
construction time. This removes the capabilities cache, the HTTP
dependency on _internal/capabilities, and simplifies the adapter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…actionEndpoints

Add ActionRef type and actions field to CollectionRef. The adapter
constructor now only takes client + collectionRefs, removing the
separate actionEndpoints parameter and the dual source of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ad of string[]

Update AgentPort interface and adapter to return ActionRef[] (name +
displayName) from getActions, making the display name available to
consumers without a separate lookup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…yKeyFields

Add primaryKeyFields to CollectionRef. buildPkFilter() generates a
single Equal filter for simple PKs, or an And condition tree for
composite PKs (separator: '|'). Fixes the hardcoded 'id' assumption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… instead of pipe-encoded string

Decouples the public API from the Forest Admin pipe encoding convention.
Pipe encoding is now an internal implementation detail of AgentClientAgentPort only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Callers no longer need to know PK field names — they pass values in
primaryKeyFields order. Pipe encoding stays internal to encodePk().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matthv matthv force-pushed the feature/prd-232-implementer-agentport-avec-agent-client branch from 6c861c5 to 2a2d898 Compare March 18, 2026 14:00
…n.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
matthv and others added 2 commits March 18, 2026 15:12
…e.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matthv matthv merged commit cb8036b into feat/prd-214-setup-workflow-executor-package Mar 18, 2026
30 checks passed
@matthv matthv deleted the feature/prd-232-implementer-agentport-avec-agent-client branch March 18, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants